package egar

import (
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
)

var (
	ErrInvalidElementType = errors.New("invalid json element type")
)

type JsonValueType uint8

const (
	TypeInvalid JsonValueType = iota
	TypeNull
	TypeBool
	TypeNumber
	TypeString
	TypeArray
	TypeObject
	TypeRaw
)

type JsonValue struct {
	Type  JsonValueType
	Value interface{}
}

func newJsonValue(valueType JsonValueType, value interface{}) *JsonValue {
	return &JsonValue{
		Type:  valueType,
		Value: value,
	}
}

func NewJsonNullValue() *JsonValue {
	return newJsonValue(TypeNull, nil)
}

func NewJsonBoolValue(value bool) *JsonValue {
	return newJsonValue(TypeBool, value)
}

func NewJsonNumberValue(value float64) *JsonValue {
	return newJsonValue(TypeNumber, value)
}

func NewJsonStringValue(value string) *JsonValue {
	return newJsonValue(TypeString, value)
}

func NewJsonArrayValue(value []*JsonValue) *JsonValue {
	return newJsonValue(TypeArray, value)
}

func NewJsonObjectValue(value map[string]*JsonValue) *JsonValue {
	return newJsonValue(TypeObject, value)
}

func (j *JsonValue) IsValid() bool {
	return j.Type != TypeInvalid
}

func (j *JsonValue) IsNull() bool {
	return j.Type == TypeNull
}

func (j *JsonValue) Bool() bool {
	if j.Type == TypeBool {
		if result, ok := j.Value.(bool); ok {
			return result
		}
	}
	panic("json value type is not bool")
}

func (j *JsonValue) Number() float64 {
	if j.Type == TypeNumber {
		if result, ok := j.Value.(float64); ok {
			return result
		}
	}
	panic("json value type is not number")
}

func (j *JsonValue) Int() int {
	return int(j.Number())
}

func (j *JsonValue) String() string {
	if j.Type == TypeString {
		if result, ok := j.Value.(string); ok {
			return result
		}
	}
	panic("json value type is not string")
}

func (j *JsonValue) Array() []*JsonValue {
	if j.Type == TypeArray {
		if result, ok := j.Value.([]*JsonValue); ok {
			return result
		}
	}
	panic("json value type is not array")
}

func (j *JsonValue) Object() map[string]*JsonValue {
	if j.Type == TypeObject {
		if result, ok := j.Value.(map[string]*JsonValue); ok {
			return result
		}
	}
	panic("json value type is not object")
}

func (j *JsonValue) Raw() []byte {
	if j.Type == TypeRaw {
		if result, ok := j.Value.([]byte); ok {
			return result
		}
	}
	panic("json value type is not raw")
}

func (j *JsonValue) GetIndex(index int) *JsonValue {
	return j.Array()[index]
}

func (j *JsonValue) GetAttribute(name string) *JsonValue {
	return j.Object()[name]
}

type formatterVisitor struct {
}

func (v *formatterVisitor) Visit(value *JsonValue) interface{} {
	return value.Accept(v)
}

func (v *formatterVisitor) VisitNull() interface{} {
	return "null"
}

func (v *formatterVisitor) VisitBool(value bool) interface{} {
	if value {
		return "true"
	} else {
		return "false"
	}
}

func (v *formatterVisitor) VisitNumber(value float64) interface{} {
	return strconv.FormatFloat(value, 'f', -1, 64)
}

func (v *formatterVisitor) VisitString(value string) interface{} {
	return "\"" + value + "\""
}

func (v *formatterVisitor) VisitArray(value []*JsonValue) interface{} {
	str := "["
	for i := range value {
		str += value[i].Format()
		if i < len(value)-1 {
			str += ","
		}
	}
	return str + "]"
}

func (v *formatterVisitor) VisitObject(value map[string]*JsonValue) interface{} {
	str := "{"
	i := 0
	for key, val := range value {
		str += "\"" + key + "\"" + ":" + val.Format()
		if i < len(value)-1 {
			str += ","
		}
		i++
	}
	return str + "}"
}

func (v *formatterVisitor) VisitRaw(value []byte) interface{} {
	return string(value)
}

func (j *JsonValue) Format() string {
	return j.Accept(&formatterVisitor{}).(string)
}

func (j *JsonValue) Accept(visitor JsonValueVisitor) interface{} {
	switch j.Type {
	case TypeNull:
		return visitor.VisitNull()
	case TypeBool:
		return visitor.VisitBool(j.Bool())
	case TypeNumber:
		return visitor.VisitNumber(j.Number())
	case TypeString:
		return visitor.VisitString(j.String())
	case TypeArray:
		return visitor.VisitArray(j.Array())
	case TypeObject:
		return visitor.VisitObject(j.Object())
	case TypeRaw:
		return visitor.VisitRaw(j.Raw())
	default:
		panic(fmt.Errorf("invalid json value type: 0x%x", j.Type))
	}
}

type JsonValueVisitor interface {
	VisitNull() interface{}
	VisitBool(value bool) interface{}
	VisitNumber(value float64) interface{}
	VisitString(value string) interface{}
	VisitArray(value []*JsonValue) interface{}
	VisitObject(value map[string]*JsonValue) interface{}
	VisitRaw(value []byte) interface{}
}

type JsonParser interface {
	Parse(data []byte) (*JsonValue, error)
	Format(value *JsonValue) ([]byte, error)
}

type jsonParserNormal struct {
}

func NewJsonParser() *jsonParserNormal {
	return &jsonParserNormal{}
}

func elementToJsonValue(element interface{}) (*JsonValue, error) {
	switch element := element.(type) {
	case nil:
		return NewJsonNullValue(), nil
	case bool:
		return NewJsonBoolValue(element), nil
	case float64:
		return NewJsonNumberValue(element), nil
	case string:
		return NewJsonStringValue(element), nil
	case []interface{}:
		values := make([]*JsonValue, 0)
		for i := range element {
			if value, err := elementToJsonValue(element[i]); err != nil {
				return nil, err
			} else {
				values = append(values, value)
			}
		}
		return NewJsonArrayValue(values), nil
	case map[string]interface{}:
		object := make(map[string]*JsonValue)
		for key := range element {
			if value, err := elementToJsonValue(element[key]); err != nil {
				return nil, err
			} else {
				object[key] = value
			}
		}
		return NewJsonObjectValue(object), nil
	}
	return nil, ErrInvalidElementType
}

func (j jsonParserNormal) Parse(data []byte) (*JsonValue, error) {
	var element interface{}
	err := json.Unmarshal(data, &element)
	if err != nil {
		return nil, err
	}
	return elementToJsonValue(element)
}

func (j jsonParserNormal) Format(value *JsonValue) string {
	return value.Format()
}
